Pressidian
花园入口
笔记
项目
关于
实验室
GitHub
花园入口
笔记
项目
关于
实验室
GitHub

KNOWLEDGE PATHS

笔记库
当前位置
笔记库/前端/三件套/JS/JavaScript

10 异步编程

22 分钟阅读 · Note

目录树 578 篇

            • 00 JavaScript学习地图
            • 01 JavaScript基础
            • 02 类型系统与类型转换
            • 03 Number与String
            • 04 Array数组
            • 05 Object对象
            • 06 集合与迭代
            • 07 函数、作用域与this
            • 08 原型、构造函数与类
            • 09 Generator与异步迭代
            • 10 异步编程
            • 11 浏览器与DOM
            • 12 二进制与文件
            • 13 标准内建对象
            • 14 模块化
            • 15 错误处理
            • 16 JSDoc
            • 90 JavaScript知识点总目录
            • 91 JavaScript修正与补充记录
          • 前端模块化
      • 前端技术栈
    • 笔记目录
    • CLAUDE.md
    • Vue 组件与 Render 函数

关联笔记 6

↗事件循环eventloop正文关联↗AbortController ⌚️正文关联↗00 JavaScript学习地图反向链接↗90 JavaScript知识点总目录反向链接↗91 JavaScript修正与补充记录反向链接↗01 JavaScript基础同一路径
  • 10 异步编程

10 异步编程

> Last Format Time:8/11/2026 21:45:39

本笔记由两个 JavaScript 教程目录中的同主题内容合并而成;全部旧知识保留在“旧笔记知识全集(按来源保留)”中,并由导航逐项索引。


当前前端开发关键要点

> [!important] 学习优先级:P0 > - 必须掌握事件循环、调用栈、任务与微任务,能够解释 Promise 回调的执行顺序。 > - 使用 async/await 组织流程,并明确串行、并行、竞态、取消、超时和错误传播。 > - 根据容错需求选择 Promise.all、allSettled、race 或 any,避免无意的串行等待。 > - 组件卸载或请求替换时使用 AbortController 等机制处理取消,防止陈旧结果覆盖新状态。


知识点导航

[现代JS教程与阮一峰ES6 · 6异步编程/Promise与回调](./10 异步编程.md)

  • 回调
  • Promise
  • Promise链
  • 错误处理
  • Promise的静态方法(API)
  • all
  • allSettled:
  • Promisification
  • 微任务
  • async/await

[廖雪峰教程 · 7浏览器/7.5 Promise](./10 异步编程.md)

  • Promise
  • 手撸代码
  • 💡 Promise 基础
  • 🤔 Promise 是如何实现异步的?
  • 执行顺序解析
  • 📌 核心要点
  • 🤝 Promise.all 实现
  • 🏎️ Promise.race 实现
  • ✨ Promise.any 实现
  • 📝 总结
  • 🛠️ 实现 Promise.map
  • 📝 使用示例
  • ✨ 进阶:增加并发限制
  • 📌 总结

[廖雪峰教程 · 7浏览器/7.6 async](./10 异步编程.md)

  • async

现代前端补充与纠错

> [!info] 修改标记 > - 【修正】:旧教程中错误、过时或容易误导的内容。 > - 【补充】:旧教程未覆盖、但当前前端开发需要掌握的内容。 > - 【修正代码】:替换或校正了旧代码示例。

> [!warning] 下方 Promise 手写题保留原教程上下文;空输入、任意 iterable、错误顺序和并发边界已直接修正并标成 【修正代码】。

事件循环

  • 【补充】 Promise 回调与 queueMicrotask 进入微任务队列;定时器、消息和用户交互通常形成任务。一次任务结束后会清空微任务,再由浏览器选择渲染时机。
  • 【补充】 微任务递归排队可能阻塞渲染和后续任务;长计算需要拆分、调度或交给 Worker。
  • 【补充】 完整浏览器模型见 事件循环eventloop。

Promise 组合器边界

  • 【修正】 Promise.all(iterable) 接受任意可迭代对象;空输入会立即以 [] fulfilled,结果保持输入顺序。
  • 【修正】 Promise.any(iterable) 空输入会以 AggregateError rejected;errors 按输入顺序排列,不按完成顺序。
  • 【补充】 allSettled 适合需要完整结果清单的场景,race 只观察第一个落定结果,并不会自动取消其余任务。
  • 【补充】 并发池必须处理空任务、非法并发数、同步抛错、结果顺序与取消信号;“限制并发”不等于“超时”或“取消”。

请求与取消

  • 【修正】 fetch 遇到 HTTP 404/500 通常仍会 fulfilled,应检查 response.ok;网络失败、CORS 拒绝或主动中止才会 reject。
  • 【补充】 用 AbortController 统一取消请求和支持 signal 的监听器;超时还应处理竞态和过期响应。见 [AbortController ⌚️](../../../浏览器&网络/浏览器环境/Web API/AbortController ⌚️.md)。
  • 【补充】 全局关注未处理拒绝可监听 unhandledrejection,但它是兜底监控,不替代调用链上的错误处理。

旧笔记知识全集(按来源保留)

现代JS教程与阮一峰ES6 · 6异步编程/Promise与回调

回调

使用回调的方式来进行异步编程:

function loadScript(src, callback) {
    console.log('finished'+src);
    setTimeout(callback, 500);
}

loadScript('1',() => {console.log('callback');});

在回调中回调:

function loadScript(src, callback) {
    console.log('finished'+src);
    setTimeout(callback, 500);
}
//回调地狱
loadScript(
    '1',
    () => {
        console.log('callback');
        loadScript(
            '2',
            () => {
                console.log('callback');
                loadScript(
                    '3',
                    () => {
                        console.log('callback');
                        loadScript(
                            '4',
                            () => {
                                console.log('callback');
                            }
                        );
                    }
                );
            }
        );
    }
);
/**
 * finished1
callback
finished2
callback
finished3
callback
finished4
callback
 */
Promise

是(单线程并发)解决异步编程回调地狱的一种方案。是一个特殊的对象,Promise对象代表一个异步操作,有三种状态:pending(进行中)、fulfilled(已成功)和rejected(已失败)。

Promise构造函数接受一个函数executor作为参数,该函数的两个参数分别是resolve和reject。它们是两个回调函数,由 JavaScript 引擎提供,不用自己部署。

function timeout(ms) {
  return new Promise((resolve, reject) => {
    setTimeout(resolve, ms, 'done');
  });
}

timeout(100).then((value) => {
  console.log(value);
});

当 executor 获得了结果,无论是早还是晚都没关系,在函数体的内部它应该调用以下回调之一:

  • resolve(value) —— 如果任务成功完成并带有结果 value。
  • reject(error) —— 如果出现了 error,error 即为 error 对象。

由 new Promise 构造器返回的 promise 对象具有以下内部属性:

  • state —— 最初是 "pending",然后在 resolve 被调用时变为 "fulfilled",或者在 reject 被调用时变为 "rejected"。
  • result —— 最初是 undefined,然后在 resolve(value) 被调用时变为 value,或者在 reject(error) 被调用时变为 error。

其中,pending->fulfilled和pending->rejected是仅有的两种状态转换,代表着异步操作的结果。executor 只能调用一个 resolve 或一个 reject。任何状态的更改都是最终的。所有其他的再对 resolve 和 reject 的调用都会被忽略

![Pasted image 20250628000757](../../../../_assets/2207343-Pasted image 20250628000757.png) resolve函数的作用是,将Promise对象的状态从“未完成”变为“成功”(即从 pending 变为 resolved),在异步操作成功时调用,并==将异步操作的结果,作为参数传递出去==

reject函数的作用是,将Promise对象的状态从“未完成”变为“失败”(即从 pending 变为 rejected),在异步操作失败时调用,并==将异步操作报出的错误,作为参数传递出去==

//用于随机生成一个图片
function Image() {
    let result = Math.random();
    if (result > 0.5) {
        this.value = true;
    } else {
        this.value = false;
    };
}

let promise = new Promise(function (resolve, reject) {

    let image = new Image();
    console.log(image.value);

    if (image.value) {
        resolve('loaded');
    } else {
        reject(new Error("shit!!!"));
    }
});

注意,调用resolve或reject并不会终结 Promise 的参数函数的执行。一般来说,调用resolve或reject以后,Promise 的使命就完成了,后继操作应该放到then方法里面,而不应该直接写在resolve或reject的后面。所以,最好在它们前面加上return语句,这样就不会有意外。

new Promise((resolve, reject) => {
  resolve(1);
  console.log(2);
}).then(r => {
  console.log(r);
});
// 2
// 1

Promise 对象的 state 和 result 属性都是内部的。我们无法直接访问它们。但我们可以对它们使用 .then/.catch/.finally 方法。我们在下面对这些方法进行了描述。

Promise实例生成以后,可以用then方法分别指定resolved状态和rejected状态的回调函数。then方法可以接受两个回调函数作为参数。第一个回调函数是Promise对象的状态变为resolved时调用,第二个回调函数是Promise对象的状态变为rejected时调用。这两个函数都是可选的。它们都接受Promise对象传出的值作为参数。

promise.then(
  function(result) { /* handle a successful result */ },
  function(error) { /* handle an error */ }
);
promise.then(
    result => { console.log(result) },//loaded
    error => { console.log(error) }//Error: shit!!!
)

.catch(f) 调用是 .then(null, f) 的完全的模拟,它只是一个==简写形式==。 一般使用then来处理结果,使用catch来处理错误。

finally 的功能是设置一个处理程序在前面的操作完成后,执行清理/终结。

new Promise((resolve, reject) => {
  return resolve(1);
  // 后面的语句不会执行
  console.log(2);
})
getJSON('/posts.json').then(function(posts) {
  // ...
}).catch(function(error) {
  // 处理 getJSON 和 前一个回调函数运行时发生的错误
  console.log('发生错误!', error);
});

上面代码中,getJSON()方法返回一个 Promise 对象,如果该对象状态变为resolved,则会调用then()方法指定的回调函数;如果异步操作抛出错误,状态就会变为rejected,就会调用catch()方法指定的回调函数,处理这个错误。另外,then()方法指定的回调函数,如果运行中抛出错误,也会被catch()方法捕获。

p.then((val) => console.log('fulfilled:', val))
  .catch((err) => console.log('rejected', err));

// 等同于
p.then((val) => console.log('fulfilled:', val))
  .then(null, (err) => console.log("rejected:", err));
// 写法二
const promise = new Promise(function(resolve, reject) {
  reject(new Error('test'));
});
promise.catch(function(error) {
  console.log(error);
});

如果 Promise 状态已经变成resolved,再抛出错误是无效的。

const promise = new Promise(function(resolve, reject) {
  resolve('ok');
  throw new Error('test');
});
promise
  .then(function(value) { console.log(value) })
  .catch(function(error) { console.log(error) });
// ok

上面代码中,Promise 在resolve语句后面,再抛出错误,不会被捕获,等于没有抛出。因为 Promise 的状态一旦改变,就永久保持该状态,不会再变了。一般来说,不要在then()方法里面定义 Reject 状态的回调函数(即then的第二个参数),总是使用catch方法。Promise 对象的错误具有“冒泡”性质,会一直向后传递,直到被捕获为止。也就是说,错误总是会被下一个catch语句捕获。

getJSON('/post/1.json').then(function(post) {
  return getJSON(post.commentURL);
}).then(function(comments) {
  // some code
}).catch(function(error) {
  // 处理前面三个Promise产生的错误
});

跟传统的try/catch代码块不同的是,如果没有使用catch()方法指定错误处理的回调函数,Promise 对象抛出的==错误不会传递到外层代码,即不会有任何反应。==

const someAsyncThing = function () {
    return new Promise(function (resolve, reject) {
        // 下面一行会报错,因为x没有声明
        resolve(x + 2);
    });
};

someAsyncThing()
    .then(function () {
        console.log('everything is great');
    })
    .catch((err) => {
        console.error(err);
    })

经过我的实验,是这样的,这样有bug了也不会报错 一般总是建议,Promise 对象后面要跟catch()方法,这样可以处理 Promise 内部发生的错误。catch()方法返回的还是一个 Promise 对象,因此后面还可以接着调用then()方法。

const someAsyncThing = function() {
  return new Promise(function(resolve, reject) {
    // 下面一行会报错,因为x没有声明
    resolve(x + 2);
  });
};

someAsyncThing()
.catch(function(error) {
  console.log('oh no', error);
})
.then(function() {
  console.log('carry on');
});
// oh no [ReferenceError: x is not defined]
// carry on

finally()方法用于指定不管 Promise 对象最后状态如何,都会执行的操作。该方法是 ES2018 引入标准的。

promise
.then(result => {···})
.catch(error => {···})
.finally(() => {···});
Promise链

使用链式调用来实现对结果和错误的精细处理:

let promise = new Promise(function (res,rej) {
    res(1);
})

promise.then((res) => {

    console.log(res);//1
    return res * 2;

}).then((res) => {

    console.log(res);//2
    return res * 2;

}).finally(() => {

    console.log('finished!!!');//finished!!!

})

![Pasted image 20250628004354](../../../../_assets/b375070-Pasted image 20250628004354.png)

如果将一个promise对象上添加多个then,他们是彼此独立的,并不是一个链式的结构。 ![Pasted image 20250628004333](../../../../_assets/22105b2-Pasted image 20250628004333.png)

.then(handler) 中所使用的处理程序(handler)可以创建并返回一个 promise。

确切地说,处理程序返回的不完全是一个 promise,而是返回的被称为 “thenable” 对象 —— 一个具有方法 .then 的任意对象。它会被当做一个 promise 来对待。

这个想法是,第三方库可以实现自己的“promise 兼容(promise-compatible)”对象。它们可以具有扩展的方法集,但也与原生的 promise 兼容,因为它们实现了 .then 方法。

let promise = fetch(url);

执行这条语句,向 url 发出网络请求并返回一个 promise。当远程服务器返回 header(是在 全部响应加载完成前)时,该 promise 使用一个 response 对象来进行 resolve。

fetch('/article/promise-chaining/user.json')
  // 当远程服务器响应时,下面的 .then 开始执行
  .then(function(response) {
    // 当 user.json 加载完成时,response.text() 会返回一个新的 promise
    // 该 promise 以加载的 user.json 为 result 进行 resolve
    return response.text();
  })
  .then(function(text) {
    // ……这是远程文件的内容
    alert(text); // {"name": "iliakan", "isAdmin": true}
  });

从 fetch 返回的 response 对象还包含 response.json() 方法,该方法可以读取远程数据并将其解析为 JSON。

// 同上,但使用 response.json() 将远程内容解析为 JSON
fetch('/article/promise-chaining/user.json')
  .then(response => response.json())
  .then(user => alert(user.name)); // iliakan,获取到了用户名
promise.then(f1, f2);

promise.then(f1).catch(f2);

这两个例子是不一样的: 不同之处在于,如果 f1 中出现 error,那么在这儿它会被 .catch 处理 .then 将 result/error 传递给下一个 .then/.catch。所以在第一个例子中,在下面有一个 catch,而在第二个例子中并没有 catch,所以 error 未被处理。

错误处理

正如我们已经注意到的,链尾端的 .catch 的表现有点像 try..catch。我们可能有许多个 .then 处理程序,然后在尾端使用一个 .catch 处理上面的所有 error。

在常规的 try..catch 中,我们可以分析 error,如果我们无法处理它,可以将其再次抛出。对于 promise 来说,这也是可以的。

如果我们在 .catch 中 throw,那么控制权就会被移交到下一个最近的 error 处理程序。如果我们处理该 error 并正常完成,那么它将继续到最近的成功的 .then 处理程序。

new Promise(function(resolve, reject) {
  setTimeout(() => {
    throw new Error("Whoops!");
  }, 1000);
}).catch(alert);

不会进行处理,这里的错误并不是在 executor 运行时生成的,而是在稍后生成的。因此,promise 无法处理它。

Promise的静态方法(API)
all

如果任意一个 promise 被 reject,由 Promise.all 返回的 promise 就会立即 reject,并且带有的就是这个 error。 假设我们希望并行执行多个 promise,并等待所有 promise 都准备就绪。

let promise = Promise.all(iterable);

Promise.all 接受一个可迭代对象(通常是一个==数组项为 promise 的数组==),并返回一个新的 promise。

当所有给定的 promise 都 resolve 时,新的 promise 才会 resolve,并且其结果数组将成为新 promise 的结果。

let names = ['iliakan', 'remy', 'jeresig'];

let requests = names.map(name => fetch(`https://api.github.com/users/${name}`));

Promise.all(requests)
  .then(responses => {
    // 所有响应都被成功 resolved
    for(let response of responses) {
      alert(`${response.url}: ${response.status}`); // 对应每个 url 都显示 200
    }

    return responses;
  })
  // 将响应数组映射(map)到 response.json() 数组中以读取它们的内容
  .then(responses => Promise.all(responses.map(r => r.json())))
  // 所有 JSON 结果都被解析:"users" 是它们的数组
  .then(users => users.forEach(user => alert(user.name)));

如果其中一个 promise 被 reject,Promise.all 就会立即被 reject,==完全忽略==列表中==其他的 promise==。它们的结果也被忽略。

例如,像上面那个例子,如果有多个同时进行的 fetch 调用,其中一个失败,其他的 fetch 操作仍然会继续执行,但是 Promise.all 将不会再关心(watch)它们。它们可能会 settle,但是它们的结果将被忽略。

Promise.all 没有采取任何措施来取消它们,因为 promise 中没有“取消”的概念。在 另一个章节 中,我们将介绍可以帮助我们解决这个问题(译注:指的是“取消” promise)的 AbortController,但它不是 Promise API 的一部分。

allSettled:

Promise.allSettled 等待所有的 promise 都被 settle,无论结果如何。结果数组会是这样的:

  • 对成功的响应,结果数组对应元素的内容为 {status:"fulfilled", value:result},
  • 对出现 error 的响应,结果数组对应元素的内容为 {status:"rejected", reason:error}。
let urls = [
  'https://api.github.com/users/iliakan',
  'https://api.github.com/users/remy',
  'https://no-such-url'
];

Promise.allSettled(urls.map(url => fetch(url)))
  .then(results => { // (*)
    results.forEach((result, num) => {
      if (result.status == "fulfilled") {
        alert(`${urls[num]}: ${result.value.status}`);
      }
      if (result.status == "rejected") {
        alert(`${urls[num]}: ${result.reason}`);
      }
    });
  });

[
  {status: 'fulfilled', value: ...response...},
  {status: 'fulfilled', value: ...response...},
  {status: 'rejected', reason: ...error object...}
]

没看完

Promisification

指将一个接受回调的函数转换为一个返回 promise 的函数。

//改造前
function loadScript(src, callback) {
  let script = document.createElement('script');
  script.src = src;

  script.onload = () => callback(null, script);
  script.onerror = () => callback(new Error(`Script load error for ${src}`));

  document.head.append(script);
}

// 用法:
// loadScript('path/script.js', (err, script) => {...})


//改造后
let loadScriptPromise = function(src) {
  return new Promise((resolve, reject) => {
    loadScript(src, (err, script) => {
      if (err) reject(err);
      else resolve(script);
    });
  });
};

// 用法:
// loadScriptPromise('path/script.js').then(...)

在实际开发中,我们可能需要 promise 化很多函数,所以使用一个 helper(辅助函数)很有意义。 我们将其称为 promisify(f):它接受一个需要被 promise 化的函数 f,并返回一个包装(wrapper)函数。

function promisify(f) {

  return function (...args) { // 返回一个包装函数(wrapper-function) (*)

    return new Promise((resolve, reject) => {
      function callback(err, result) { // 我们对 f 的自定义的回调 (**)
        if (err) {
          reject(err);
        } else {
          resolve(result);
        }

      }

      args.push(callback); // 将我们的自定义的回调附加到 f 参数(arguments)的末尾
      f.call(this, ...args); // 调用原始的函数
    });

  };

}

// 用法:
let loadScriptPromise = promisify(loadScript);
loadScriptPromise(...).then(...);
微任务

promise 的处理程序 .then、.catch 和 .finally 都是异步的。 即便一个 promise 立即被 resolve,.then、.catch 和 .finally 在promise后的代码也会在这些处理程序之前被执行:

let promise = Promise.resolve();

promise.then(() => alert("promise done!"));

alert("code finished"); // 这个 alert 先显示

异步任务需要适当的管理。为此,ECMA 标准规定了一个内部队列 PromiseJobs,通常被称为“微任务队列(microtask queue)”(V8 术语):

  • 队列(queue)是先进先出的:首先进入队列的任务会首先运行。
  • 只有在 JavaScript 引擎中没有其它任务在运行时,才开始执行任务队列中的任务。

当一个 promise 准备就绪时,它的 .then/catch/finally 处理程序就会被放入队列中:但是它们不会立即被执行。当 JavaScript 引擎执行完当前的代码,它会从队列中获取任务并执行它。 ![Pasted image 20250628135940](../../../../_assets/18a7f8b-Pasted image 20250628135940.png)

如果一个 promise 的 error 未被在微任务队列的末尾进行处理,则会出现“未处理的 rejection”。

async/await

promise在解决地狱回调的同时,但在使用时有一些不优雅 async/await 是以更舒适的方式使用 promise 的一种特殊语法,同时它也非常易于理解和使用。

使用async关键字在函数前,函数就会总是返回一个promise对象。其他值将自动被包装在一个 resolved 的 promise 中。

await只在 async 函数内工作,关键字 await 让 JavaScript 引擎等待直到 promise 完成(settle)并返回结果

像 promise.then 那样,await 允许我们使用 thenable 对象(那些具有可调用的 then 方法的对象)。这里的想法是,第三方对象可能不是一个 promise,但却是 promise 兼容的:如果这些对象支持 .then,那么就可以对它们使用 await。

要声明一个 class 中的 async 方法,只需在对应方法前面加上 async 即可:

class Waiter {
  async wait() {
    return await Promise.resolve(1);
  }
}

new Waiter()
  .wait()
  .then(alert); // 1(alert 等同于 result => alert(result))
async function f() {

  try {
    let response = await fetch('http://no-such-url');
  } catch(err) {
    alert(err); // TypeError: failed to fetch
  }
}

f();

async function f() {
  let response = await fetch('http://no-such-url');
}

// f() 变成了一个 rejected 的 promise
f().catch(alert); // TypeError: failed to fetch // (*)

当我们使用 async/await 时,几乎就不会用到 .then 了,因为 await 为我们处理了等待。并且我们使用常规的 try..catch 而不是 .catch。这通常更加方便。

使用同步的写法来实现异步的效果,十分的优雅 ![Pasted image 20250830122151](../../../../_assets/9d18aa9-Pasted image 20250830122151.png)

![Pasted image 20250830122623](../../../../_assets/ca02715-Pasted image 20250830122623.png) 这个函数中使用 await 是因为 axios(...) 会返回一个 Promise 对象,而 await 的作用正是等待这个 Promise 完成(成功或失败),并获取其返回结果。

具体原因如下:

  1. axios 是基于 Promise 的请求库,调用 axios(...) 后会立即返回一个 Promise(此时请求正在异步执行),而非直接返回接口响应数据。

  2. 若不使用 await,代码会继续执行后续的 list.value = res.data.list,但此时 res 还未获取到真正的响应结果(因为请求还没完成),会导致 res 是一个未完成的 Promise,进而报错。

  3. 使用 await 后,函数会暂停执行,等待 axios 的请求完成并拿到响应结果后,再将结果赋值给 res,确保后续代码能正确使用接口返回的数据。

简单说,await 是为了将异步请求的结果 “同步化” 处理,让代码按照 “发送请求→等待结果→处理结果” 的逻辑顺序执行,避免异步操作导致的数据获取时机问题。

廖雪峰教程 · 7浏览器/7.5 Promise

Promise

在JavaScript的世界中,所有代码都是单线程执行的。

由于这个“缺陷”,导致JavaScript的所有网络操作,浏览器事件,都必须是异步执行。异步执行可以用回调函数实现:

function callback() {
    console.log('Done');
}
console.log('before setTimeout()');
setTimeout(callback, 1000); // 1秒钟后调用callback函数
console.log('after setTimeout()');

观察上述代码执行,在Chrome的控制台输出可以看到:

before setTimeout()
after setTimeout()
(等待1秒后)
Done

可见,异步操作会在将来的某个时间点触发一个函数调用。

AJAX就是典型的异步操作:

request.onreadystatechange = function () {
    if (request.readyState === 4) {
        if (request.status === 200) {
            return success(request.responseText);
        } else {
            return fail(request.status);
        }
    }
}

把回调函数success(request.responseText)和fail(request.status)写到一个AJAX操作里很正常,但是不好看,而且不利于代码复用。

有没有更好的写法?比如写成这样:

let ajax = ajaxGet('http://...');
ajax.ifSuccess(success)
    .ifFail(fail);

这种链式写法的好处在于,先统一执行AJAX逻辑,不关心如何处理结果,然后,根据结果是成功还是失败,在将来的某个时候调用success函数或fail函数。

古人云:“君子一诺千金”,这种“承诺将来会执行”的对象在JavaScript中称为Promise对象。

Promise有各种开源实现,在ES6中被统一规范,由浏览器直接支持。先测试一下你的浏览器是否支持Promise:

Run

我们先看一个最简单的Promise例子:生成一个0-2之间的随机数,如果小于1,则等待一段时间后返回成功,否则返回失败:

function test(resolve, reject) {
    let timeOut = Math.random() * 2;
    log('set timeout to: ' + timeOut + ' seconds.');
    setTimeout(function () {
        if (timeOut < 1) {
            log('call resolve()...');
            resolve('200 OK');
        }
        else {
            log('call reject()...');
            reject('timeout in ' + timeOut + ' seconds.');
        }
    }, timeOut * 1000);
}

这个test()函数有两个参数,这两个参数都是函数,如果执行成功,我们将调用resolve('200 OK'),如果执行失败,我们将调用reject('timeout in ' + timeOut + ' seconds.')。可以看出,test()函数只关心自身的逻辑,并不关心具体的resolve和reject将如何处理结果。

有了执行函数,我们就可以用一个Promise对象来执行它,并在将来某个时刻获得成功或失败的结果:

let p1 = new Promise(test);
let p2 = p1.then(function (result) {
    console.log('成功:' + result);
});
let p3 = p2.catch(function (reason) {
    console.log('失败:' + reason);
});

变量p1是一个Promise对象,它负责执行test函数。由于test函数在内部是异步执行的,当test函数执行成功时,我们告诉Promise对象:

// 如果成功,执行这个函数:
p1.then(function (result) {
    console.log('成功:' + result);
});

当test函数执行失败时,我们告诉Promise对象:

p2.catch(function (reason) {
    console.log('失败:' + reason);
});

Promise对象可以串联起来,所以上述代码可以简化为:

new Promise(test).then(function (result) {
    console.log('成功:' + result);
}).catch(function (reason) {
    console.log('失败:' + reason);
});

可见Promise最大的好处是在异步执行的流程中,把执行代码和处理结果的代码清晰地分离了:

                                   ┌──────────────────┐
                           then    │on_resolve(data) {│
                       ┌──────────▶│    // TODO       │
                       │           │}                 │
┌────────────────────────┐         └──────────────────┘
│   new Promise(async)   │
└────────────────────────┘         ┌──────────────────┐
             │         │           │on_reject(data) { │
             │         └──────────▶│    // TODO       │
      promise│             catch   │}                 │
             │                     └──────────────────┘
             ▼
┌────────────────────────┐
│async(resolve, reject) {│
│    // TODO             │
│}                       │
└────────────────────────┘

Promise还可以做更多的事情,比如,有若干个异步任务,需要先做任务1,如果成功后再做任务2,任何任务失败则不再继续并执行错误处理函数。

要串行执行这样的异步任务,不用Promise需要写一层一层的嵌套代码。有了Promise,我们只需要简单地写:

job1.then(job2).then(job3).catch(handleError);

其中,job1、job2和job3都是Promise对象。

除了串行执行若干异步任务外,Promise还可以并行执行异步任务。

试想一个页面聊天系统,我们需要从两个不同的URL分别获得用户的个人信息和好友列表,这两个任务是可以并行执行的,用Promise.all()实现如下:

let p1 = new Promise(function (resolve, reject) {
    setTimeout(resolve, 500, 'P1');
});
let p2 = new Promise(function (resolve, reject) {
    setTimeout(resolve, 600, 'P2');
});
// 同时执行p1和p2,并在它们都完成后执行then:
Promise.all([p1, p2]).then(function (results) {
    console.log(results); // 获得一个Array: ['P1', 'P2']
});

有些时候,多个异步任务是为了容错。比如,同时向两个URL读取用户的个人信息,只需要获得先返回的结果即可。这种情况下,用Promise.race()实现:

let p1 = new Promise(function (resolve, reject) {
    setTimeout(resolve, 500, 'P1');
});
let p2 = new Promise(function (resolve, reject) {
    setTimeout(resolve, 600, 'P2');
});
Promise.race([p1, p2]).then(function (result) {
    console.log(result); // 'P1'
});

由于p1执行较快,Promise的then()将获得结果'P1'。p2仍在继续执行,但执行结果将被丢弃。

手撸代码

如果我们组合使用Promise,就可以把很多异步任务以并行和串行的方式组合起来执行。

要理解这些 Promise 方法的实现,我们可以从最基础的 Promise 开始,然后逐步深入到 Promise.all、Promise.race 和 Promise.any。

首先需要说明的是,JavaScript 原生并没有 Promise.map 方法。通常所说的 Promise.map 是指对数组中的每个元素执行一个返回 Promise 的函数,并处理这些 Promise。Promise.all 就是实现这一功能的绝佳工具。

下面将通过代码示例,带你一步步了解它们的核心实现原理。

💡 Promise 基础

Promise 是一个代表异步操作最终完成(或失败)及其结果值的对象。一个简单的实现如下:

class SimplePromise {
  constructor(executor) {
    this.state = 'pending'; // 状态:pending, fulfilled, rejected
    this.value = undefined; // 成功时的值
    this.reason = undefined; // 失败时的原因
    this.onFulfilledCallbacks = []; // 成功回调队列
    this.onRejectedCallbacks = []; // 失败回调队列

    const resolve = (value) => {
      if (this.state === 'pending') {
        this.state = 'fulfilled';
        this.value = value;
        this.onFulfilledCallbacks.forEach(fn => fn());
      }
    };

    const reject = (reason) => {
      if (this.state === 'pending') {
        this.state = 'rejected';
        this.reason = reason;
        this.onRejectedCallbacks.forEach(fn => fn());
      }
    };

    try {
      executor(resolve, reject);
    } catch (error) {
      reject(error);
    }
  }

  then(onFulfilled, onRejected) {
    // 简化版 then 方法,处理状态和回调
    if (this.state === 'fulfilled') {
      onFulfilled(this.value);
    }
    if (this.state === 'rejected') {
      onRejected(this.reason);
    }
    if (this.state === 'pending') {
      this.onFulfilledCallbacks.push(() => onFulfilled(this.value));
      this.onRejectedCallbacks.push(() => onRejected(this.reason));
    }
  }
}

在这段代码中,executor(执行器)是你传递给 Promise 构造函数的那个函数。

当你使用 new Promise((resolve, reject) =&gt; { ... }) 时,箭头函数 (resolve, reject) =&gt; { ... } 就是这个 executor。

你说得对,我们之前实现的 Promise 中的 executor 函数确实是同步执行的。但这并不矛盾,因为 Promise 的异步能力,并非来自于 executor 本身,而是来自于 executor 内部通常会调用的那些真正的异步 API。

简单来说,Promise 本身只是一个“包装器”或“管理器”,它负责管理和通知异步操作的结果,但它自己并不创造异步。

支持链式调用的promise

class ChainablePromise {
    constructor(executor) {
        this.state = 'pending'
        this.value = undefined
        this.reason = undefined
        this.onFulfilledCallbacks = []
        this.onRejectedCallbacks = []

        const resolve = (value) => {
            if (this.state === 'pending') {
                this.state = 'fulfilled'
                this.value = value
                this.onFulfilledCallbacks.forEach((fn) => fn())
            }
        }

        const reject = (reason) => {
            if (this.state === 'pending') {
                this.state = 'rejected'
                this.reason = reason
                this.onRejectedCallbacks.forEach((fn) => fn())
            }
        }

        try {
            executor(resolve, reject)
        } catch (error) {
            reject(error)
        }
    }

    // 核心改造:then 方法
    then(onFulfilled, onRejected) {
        // 处理未传入回调的情况 - 需要将函数设为默认或透传
        onFulfilled =
            typeof onFulfilled === 'function' ? onFulfilled : (value) => value
        onRejected =
            typeof onRejected === 'function'
                ? onRejected
                : (reason) => {
                      throw reason
                  }

        // 1. 关键!每次调用 .then() 都必须返回一个新的 Promise
        return new ChainablePromise((resolve, reject) => {
            // 2. 定义一个函数来处理 onFulfilled 回调的执行和结果
            const handleFulfillment = () => {
                try {
                    // 执行用户传入的成功回调
                    const result = onFulfilled(this.value)
                    // 检查回调的返回值
                    if (result instanceof ChainablePromise) {
                        // 如果返回的是一个 Promise,就等待它完成
                        result.then(resolve, reject)
                    } else {
                        // 如果返回的是普通值,就用这个值 resolve 新的 Promise
                        resolve(result)
                    }
                } catch (error) {
                    // 如果回调函数内部抛出异常,则 reject 新的 Promise
                    reject(error)
                }
            }

            // 3. 定义一个函数来处理 onRejected 回调
            const handleRejection = () => {
                try {
                    const result = onRejected(this.reason)
                    if (result instanceof ChainablePromise) {
                        result.then(resolve, reject)
                    } else {
                        resolve(result)
                    }
                } catch (error) {
                    reject(error)
                }
            }

            // 4. 根据当前 Promise 的状态,决定何时执行回调
            if (this.state === 'fulfilled') {
                handleFulfillment()
            }
            if (this.state === 'rejected') {
                handleRejection()
            }
            if (this.state === 'pending') {
                // 如果还是 pending 状态,就将回调函数存入队列,等待未来执行
                this.onFulfilledCallbacks.push(handleFulfillment)
                this.onRejectedCallbacks.push(handleRejection)
            }
        })
    }

    catch(onRejected) {
        return this.then(null, onRejected)
    }

    finally(onFinally) {
        return this.then(
            (value) => {
                onFinally()
                return value
            },
            (reason) => {
                onFinally()
                throw reason
            },
        )
    }
}

const myPromise = new ChainablePromise((resolve, reject) => {
    setTimeout(() => {
        resolve('success')
    }, 2000)

    setTimeout(() => {
        reject('error')
    }, 3000)
})
    .then((res) => {
        console.log('success:', res)
    })
    .catch((err) => {
        console.log('error:', err)
    }).finally(() => {
        console.log('finally')
    })

一但递归起来就有点搞不明白了,呵呵呵

🤔 Promise 是如何实现异步的?

Promise 的异步能力是“借力”而来的。它依赖于 JavaScript 运行时环境(比如浏览器或 Node.js)提供的原生异步 API。

最常见的就是 setTimeout、XMLHttpRequest (或 fetch)、fs.readFile 等。

让我们来看一个例子:

const myPromise = new Promise((resolve, reject) => {
  // 👆 executor 函数是同步执行的

  console.log('1. executor 开始执行 (同步)');

  // 2. 调用浏览器提供的异步 API
  setTimeout(() => {
    console.log('3. setTimeout 的回调执行 (异步)');
    resolve('任务完成');
  }, 1000);

  console.log('2. executor 执行完毕 (同步)');
});

console.log('4. Promise 创建完毕,但异步任务还在后台进行');

myPromise.then(result => {
  console.log('5. Promise 状态变为 fulfilled,then 回调执行', result);
});

我理解了,promise其实就是将我们还没有执行的逻辑放在了待执行的队列中,在pendding的状态结束的时候就回执行这些转态,太妙了。

执行顺序解析
  1. executor 同步执行:new Promise() 被调用,executor 函数立即开始执行。
  2. 调用 setTimeout:executor 内部调用了 setTimeout。setTimeout 是浏览器提供的一个异步 Web API。当 setTimeout 被调用时,它会告诉浏览器的计时器线程:“1秒后,把这个回调函数放进任务队列里”。然后,setTimeout 函数本身会立即返回,不会阻塞后续代码。
  3. executor 执行完毕:executor 中的同步代码执行完毕,Promise 对象创建完成,状态仍为 pending。
  4. 主线程继续:主线程继续执行,打印第4行日志。
  5. 异步回调入队:1秒后,浏览器的计时器线程将 setTimeout 的回调函数(也就是那个包含 resolve('任务完成') 的函数)放入任务队列 (Task Queue)。
  6. 事件循环 (Event Loop) 调度:JavaScript 的事件循环会不断检查调用栈是否为空。当主线程的同步任务都完成后,事件循环会从任务队列中取出回调函数,并将其放入调用栈执行。
  7. resolve 被调用:setTimeout 的回调函数执行,调用了 resolve。这时,Promise 的状态才从 pending 变为 fulfilled。
  8. then 回调执行:Promise 状态改变后,.then() 方法注册的回调函数被放入微任务队列,等待执行,最终打印第5行日志。
📌 核心要点
  • executor 是同步的:它的作用是立即启动一个异步任务。
  • 异步的来源:真正的异步行为是由 executor 内部调用的原生异步 API(如 setTimeout、fetch 等)提供的。
  • Promise 的角色:Promise 是一个“状态机”和“回调管理器”。它通过 resolve 和 reject 函数,将异步操作的结果“通知”给后续的 .then() 或 .catch() 回调。

所以,Promise 之所以能处理异步,是因为它巧妙地包装了那些底层的、真正的异步机制,并提供了一套更优雅、更易用的链式调用语法。

🤝 Promise.all 实现

8/11/26 【修正代码】原内容:只支持带 length/forEach 的数组,且空输入永远 pending;修改点:先接收任意 iterable,并让空输入立即 fulfilled 为 []。Promise.all 接收一个 Promise 可迭代对象(如数组),并返回一个新的 Promise。

  • 成功:当所有传入的 Promise 都成功时,新 Promise 才会成功,结果是一个按原始顺序排列的值数组。
  • 失败:只要有一个 Promise 失败,新 Promise 就会立即失败,并带上第一个失败的原因。
Promise.myAll = function(iterable) {
  return new Promise((resolve, reject) => {
    const promises = Array.from(iterable);
    const results = new Array(promises.length);
    let completedCount = 0;

    if (promises.length === 0) {
      resolve([]);
      return;
    }

    promises.forEach((promise, index) => {
      Promise.resolve(promise).then(value => {
        results[index] = value;
        completedCount++;
        if (completedCount === promises.length) resolve(results);
      }, reject);
    });
  });
};
🏎️ Promise.race 实现

Promise.race 同样接收一个 Promise 可迭代对象,并返回一个新的 Promise。

  • 竞速:返回的 Promise 会随着第一个 Promise 的敲定(无论是成功还是失败)而敲定。
Promise.myRace = function(promises) {
  return new Promise((resolve, reject) => {
    // 1. 遍历所有 Promise
    promises.forEach(promise => {
      // 2. 使用 Promise.resolve 包装,确保处理非 Promise 值
      Promise.resolve(promise)
        .then(resolve) // 3. 任何一个 Promise 成功,立即 resolve
        .catch(reject); // 4. 任何一个 Promise 失败,立即 reject
    });
  });
};
✨ Promise.any 实现

8/11/26 【修正代码】原内容:空输入永远 pending,错误按完成顺序 push;修改点:空输入拒绝为 AggregateError,错误按输入索引保存。Promise.any 是 ES2021 引入的新方法,它接收一个 Promise 可迭代对象,并返回一个新的 Promise。

  • 成功:只要有一个 Promise 成功,新 Promise 就会立即成功,并带上第一个成功的值。
  • 失败:只有当所有传入的 Promise 都失败时,新 Promise 才会失败,并带有一个 AggregateError,其中包含了所有失败的原因。
Promise.myAny = function(iterable) {
  return new Promise((resolve, reject) => {
    const promises = Array.from(iterable);
    const errors = new Array(promises.length);
    let rejectedCount = 0;

    if (promises.length === 0) {
      reject(new AggregateError([], 'All promises were rejected'));
      return;
    }

    promises.forEach((promise, index) => {
      Promise.resolve(promise).then(resolve, error => {
        errors[index] = error;
        rejectedCount++;
        if (rejectedCount === promises.length) {
          reject(new AggregateError(errors, 'All promises were rejected'));
        }
      });
    });
  });
};
📝 总结
方法成功条件失败条件返回结果
Promise.all所有 Promise 都成功任一 Promise 失败所有成功值的数组(按原始顺序)
Promise.race第一个 Promise 敲定(成功或失败)第一个 Promise 敲定(成功或失败)第一个敲定 Promise 的值或原因
Promise.any第一个 Promise 成功所有 Promise 都失败第一个成功的值,或包含所有错误的 AggregateError
正如我们之前提到的,JavaScript 原生并没有 Promise.map 方法。这个概念通常源于像 Bluebird 这样的第三方 Promise 库。

实现一个 Promise.map 的核心思路是:将一个数组,通过一个返回 Promise 的函数,转换成一个新的 Promise,这个新 Promise 的结果是一个由所有子 Promise 结果组成的数组。

我们可以利用之前讨论过的 Promise.all 来优雅地实现它。

🛠️ 实现 Promise.map

Promise.map 通常会接收两个参数:

  1. 一个数组(或可迭代对象)。
  2. 一个“映射函数”,该函数会对数组中的每个元素进行处理,并返回一个 Promise。

它的实现非常简单,核心就是将 Array.prototype.map 和 Promise.all 结合起来。

/**
 * 实现一个 Promise.map 方法
 * @param {Array} list - 需要处理的数组
 * @param {Function} iteratee - 映射函数,接收 (item, index) 并应返回一个 Promise
 * @returns {Promise} - 一个 Promise,resolve 时返回所有结果的数组
 */
Promise.myMap = function(list, iteratee) {
  // 1. 使用原生 Array.map 遍历列表,对每个元素应用 iteratee 函数
  // 这会生成一个由 Promise 组成的新数组
  const promisesArray = list.map((item, index) => {
    return iteratee(item, index);
  });

  // 2. 使用 Promise.all 等待这个 Promise 数组中的所有 Promise 完成
  // Promise.all 返回的 Promise 会在所有子 Promise 成功后 resolve,结果为值数组
  return Promise.all(promisesArray);
};
📝 使用示例

下面是一个具体的例子,模拟一个异步操作(例如从 API 获取数据),并使用我们实现的 Promise.myMap 来处理一个 ID 列表。

// 模拟一个异步函数,比如从服务器获取用户信息
function fetchUser(id) {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      if (id > 0) {
        resolve({ id: id, name: `用户${id}` });
      } else {
        reject(new Error(`无效的用户ID: ${id}`));
      }
    }, 1000);
  });
}

const userIds = [1, 2, 3];

// 使用我们实现的 Promise.myMap
Promise.myMap(userIds, (id) => {
  // 这里的函数会返回一个 Promise
  console.log(`正在获取 ID 为 ${id} 的用户...`);
  return fetchUser(id);
})
.then(users => {
  console.log('所有用户获取成功:', users);
  // 输出: 所有用户获取成功: [ { id: 1, name: '用户1' }, { id: 2, name: '用户2' }, { id: 3, name: '用户3' } ]
})
.catch(error => {
  console.error('获取用户失败:', error);
  // 如果任何一个 fetchUser 失败,这里会捕获到错误
});
✨ 进阶:增加并发限制

8/11/26 【修正代码】原内容:空数组不 resolve、非法并发数会卡住、同步抛错与拒绝后继续调度未处理;修改点:补齐输入转换、边界校验、空输入、同步错误和停止调度。 一个更强大的 Promise.map(如 Bluebird 库中的实现)通常会支持并发限制,即同时最多只执行 N 个 Promise。这对于处理大量任务,避免耗尽系统资源(如内存或网络连接)非常有用。

实现带并发限制的 Promise.map 会更复杂一些,需要一个队列来管理任务的执行。

/**
 * 实现一个带并发限制的 Promise.map
 * @param {Array} list - 需要处理的数组
 * @param {Function} iteratee - 映射函数
 * @param {Object} options - 选项,例如 { concurrency: 2 } 表示最多同时执行2个
 * @returns {Promise}
 */
Promise.myMapWithConcurrency = function(iterable, iteratee, options = {}) {
  const list = Array.from(iterable);
  const concurrency = options.concurrency ?? Infinity;

  if (!(concurrency === Infinity || (Number.isInteger(concurrency) && concurrency > 0))) {
    return Promise.reject(new RangeError('concurrency must be a positive integer or Infinity'));
  }

  const results = new Array(list.length);
  let nextIndexToProcess = 0;
  let completedCount = 0;
  let activeCount = 0;
  let settled = false;

  return new Promise((resolve, reject) => {
    if (list.length === 0) {
      resolve([]);
      return;
    }

    function startNext() {
      while (!settled && nextIndexToProcess < list.length && activeCount < concurrency) {
        const index = nextIndexToProcess++;
        activeCount++;
        Promise.resolve()
          .then(() => iteratee(list[index], index))
          .then(value => {
            results[index] = value;
            completedCount++;
            activeCount--;
            if (completedCount === list.length) {
              settled = true;
              resolve(results);
            } else {
              startNext();
            }
          }, error => {
            settled = true;
            reject(error);
          });
      }
    }

    startNext();
  });
};
📌 总结
  • 基础实现:Promise.map(list, fn) 的核心就是 Promise.all(list.map(fn))。它简单易用,但会同时启动所有异步任务。
  • 进阶实现:带并发限制的 Promise.map 更加健壮,适合在生产环境中处理大量任务,通过一个内部的任务队列和计数器来精确控制同时运行的 Promise 数量。

廖雪峰教程 · 7浏览器/7.6 async

async

可以用关键字async配合await调用Promise,实现异步操作,但代码却和同步写法类似:

async function get(url) {
    let resp = await fetch(url);
    let result = await resp.json();
    return result;
}

使用async function可以定义一个异步函数,异步函数和Promise可以看作是等价的,在async function内部,用await调用另一个异步函数,写起来和同步代码没啥区别,但执行起来是异步的:

let resp = await fetch(url);

自动实现了异步调用,它和下面的Promise代码等价:

let promise = fetch(url);
promise.then((resp) => {
    // 拿到resp
});

如果我们要实现catch()怎么办?用Promise的写法如下:

let promise = fetch(url);
promise.then((resp) => {
    // 拿到resp
}).catch(e => {
    // 出错了
});

用await调用时,直接用传统的try { ... } catch:

async function get(url) {
    try {
        let resp = await fetch(url);
        let result = await resp.json();
        return result;
    } catch (e) {
        // 出错了
    }
}

用async定义异步函数,用await调用异步函数,写起来和同步代码差不多,但可读性大大提高。